# Reksio - Memory Map Editor
# Copyright (C) 2023 CERN
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
# In applying this licence, CERN does not waive the privileges and immunities
# granted to it by virtue of its status as an Intergovernmental Organization or
# submit itself to any jurisdiction.

import reksio

from on_load_node import add_attrib_if_not_exists, add_attrib_container_if_not_exists

import sys
from pathlib import Path
from functools import reduce

def lookup(root, type):
	results = []
	if root.type == type and root.type != 'address-space':
		results.append(root)
	for child in root.children():
		results = results + lookup(child, type)
	return results

def lookup_postorder(root, type):
    for child in root.children():
        yield from lookup_postorder(child, type)
    if root.type == type:
        yield root

def get_semantic_version_from_user(main_window, msg, propagated_change=None):
    box = reksio.QMessageBox(main_window)
    if propagated_change is not None:
        box.setText(f"Please describe what type of changes were made to {msg}.\n Propagated change: {propagated_change}")
    else:
        box.setText(f"Please describe what type of changes were made to {msg}")
    box.setIcon(reksio.QMessageBox.Icon.Question)
    ignore_btn = box.addButton(reksio.QMessageBox.StandardButton.Ignore)
    cancel_btn = box.addButton(reksio.QMessageBox.StandardButton.Cancel)

    major_btn = box.addButton("Major", reksio.QMessageBox.ButtonRole.ActionRole)
    minor_btn = box.addButton("Minor", reksio.QMessageBox.ButtonRole.ActionRole)
    patch_btn = box.addButton("Patch", reksio.QMessageBox.ButtonRole.ActionRole)

    box.exec()

    clicked_btn = box.clickedButton()

    if clicked_btn == major_btn:
        return "major"
    elif clicked_btn == minor_btn:
        return "minor"
    elif clicked_btn == patch_btn:
        return "patch"
    elif clicked_btn == cancel_btn:
        return "cancel"
    elif clicked_btn == ignore_btn:
        return "ignore"
    else:
        raise Exception("Unknown button pressed!")

def increment_semantic_version(main_window, change_type, node):
    nodes_model = main_window.getNodesModel()
    node_index = nodes_model.getIndex(node)
    attributes_model = nodes_model.getAttributesModel(node_index)
    semantic_version_attr = node.getAttribute( ['x-cern-info', 'semantic-mem-map-version'] )
    if semantic_version_attr:
        semantic_version_attr_index = attributes_model.getIndex(semantic_version_attr)
        sem_ver = semantic_version_attr.value
        if sem_ver != "":
            major, minor, patch = map(int, sem_ver.split("."))

            if change_type == "major":
                new_sem_ver = f"{major+1}.0.0"
            elif change_type == "minor":
                new_sem_ver = f"{major}.{minor+1}.0"
            else:
                new_sem_ver = f"{major}.{minor}.{patch+1}"

            reksio.changeAttributeCommand(semantic_version_attr_index, node_index, new_sem_ver)

        else:
            # empty
            # set to 0.0.1
            reksio.changeAttributeCommand(semantic_version_attr_index, node_index, "0.0.1")
    else:
        # doesnt exist
        # add attribute

        # check if it has x-cern-info
        node_attrib_cont = node.attribute_container()
        if not node_attrib_cont:
            raise Exception("node attrib container is None")
        node_attrib_cont_index = attributes_model.getIndex(node_attrib_cont)
        cern_info_container = node_attrib_cont.getAttributeContainer("x-cern-info")

        if not cern_info_container:
            # we need to add x-cern-info
            reksio.addAttributeContainerCommand(attributes_model.rowCount(node_attrib_cont_index), "x-cern-info", node_index, node_attrib_cont_index, attributes_model)
            cern_info_container = node_attrib_cont.getAttributeContainer("x-cern-info")

        if not cern_info_container:
            raise Exception("failed to add attribute container")
        cern_info_container_index = attributes_model.getIndex(cern_info_container)

        reksio.addAttributeCommand("semantic-mem-map-version", node_index, cern_info_container_index, attributes_model, "0.0.1")

def expand(main_window):
    nodes_model = main_window.getNodesModel()
    if not nodes_model:
        return
    root_node = nodes_model.getRoot()
    already_processed_submap_files = set()
    if main_window.isModified():
        # everything should be saved before, as weird things may happen to undo/redo stack, if we're in clean state, but have commands that can be redone (especially for those submaps)
        reksio.popup_warning("Unable to reload submaps!", f"Please save your changes made or undo them in order to reload submaps.")
        return False
    else:
        # just to be sure -  remove all redo commands
        main_window.getUndoStack().clear()
        # the fix would be to get list of QUndoCommands that can be redone and concern submaps and set them as obsolete
    # select root element!!! in case something selected gets removed
    nodes_view = main_window.getNodesView()
    #nodes_view.selectRootIndex()
    nodes_view.collapseAll() #doesnt work
    top_item = root_node.getChildByType("memory-map")
    top_item_index = nodes_model.getIndex(top_item)
    main_window.nodesViewCurrentChanged(top_item_index)
    remove_expanded_submaps(main_window, root_node)
    nodes_model.clearAttributesModels()
    return expand_submaps(root_node, nodes_model, main_window, already_processed_submap_files)

def remove_expanded_submaps(main_window, root_node):
    nodes_model = main_window.getNodesModel()
    submap_list = lookup(root_node, "submap")
    for submap in submap_list:
        memory_map_node = submap.getChildByType("memory-map")
        if memory_map_node:
            # already loaded, needs to be deleted first
            #submap.removeChild(memory_map_node)
            nodes_model.removeChild(memory_map_node.index(), nodes_model.getIndex(submap))
            # check if removed
            memory_map_node = submap.getChildByType("memory-map")
            if memory_map_node:
                raise Exception(f"memory-map node of {submap} should be gone, but it's not!")
            # submap_list is now invalid, do it again
            remove_expanded_submaps(main_window, root_node)
            break
    return True

def set_computed_filename(submap, filename):
    add_attrib_container_if_not_exists(submap, "computed")
    computed_attr_container = submap.attribute_container().getAttributeContainer("computed")
    add_attrib_if_not_exists(computed_attr_container, "file_path")

    file_path_attr = submap.getAttribute(["computed", "file_path"])
    file_path_attr.value = str(filename)

    reksio.get_main_window().dataChanged(file_path_attr)


def expand_submaps(root_node, nodes_model, main_window, already_processed_submap_files):
    submap_list = lookup(root_node, "submap")
    if not submap_list:
        return True
    for submap in submap_list:
        submap_index = nodes_model.getIndex(submap)
        memory_map_node = submap.getChildByType("memory-map")
        if memory_map_node:
            raise Exception(f"Trying to expand submap {submap} while it's already expanded!")
        try:
            working_path = get_submap_working_path(submap, main_window)
            filename = submap["filename"].value.replace("\\", "/")
            filename = (working_path / filename).resolve()
            set_computed_filename(submap, filename)
        except KeyError:
            continue
        try:
            data = reksio.loadMap(str(filename), main_window)
        except reksio.YAMLFileNotFound:
            reksio.critical(f"File not found: {filename} when expanding submap {submap}")
            continue
        # only single instance of a submap can be edited, in order to avoid conflicts
        if filename in already_processed_submap_files:
            data[0].editable = False
        else:
            already_processed_submap_files.add(filename)
        data[0].savable = False
        submap.addChild(data[0])
        memory_map_node = submap.getChildByType("memory-map")
        if not memory_map_node:
            raise Exception(f"Something went really wrong while loading submap {submap}")
        expand_submaps(memory_map_node, nodes_model, main_window, already_processed_submap_files)
        #main_window.callPythonOnLoadScript(memory_map_node)
        submap.validate(True)

        main_window.dataChanged(submap)
    return True

def is_direct_savable_parent(main_window, parent_index, modified_index):
    nodes_model = main_window.getNodesModel()
    tmp_parent = modified_index
    while tmp_parent.isValid():
        if parent_index == tmp_parent:
            return True
        if nodes_model.getItem(tmp_parent).type == "memory-map":
            break
        tmp_parent = tmp_parent.parent()
    return False

def is_submap_modified(main_window, submap, modified_indexes=None):
    nodes_model = main_window.getNodesModel()

    memory_map_node = submap.getChildByType("memory-map")
    if not memory_map_node:
        # not loaded submap? not modified
        return False
    memory_map_index = nodes_model.getIndex(memory_map_node)

    if modified_indexes is None:
        modified_indexes = nodes_model.getModifiedIndexes()
    for modified_index in modified_indexes:
        if is_direct_savable_parent(main_window, memory_map_index, modified_index):
            return True
    return False


def is_any_submap_modified(main_window, submaps=None):
    nodes_model = main_window.getNodesModel()
    modified_indexes = nodes_model.getModifiedIndexes()
    for submap in submaps:
            if is_submap_modified(main_window, submap, modified_indexes):
                return True
    return False

def get_submap_working_path(submap_node, main_window):
    if not main_window.hasFile():
        raise Exception("main_window has to have a file before using this function")

    currentFile = Path(str(main_window.currentFile))
    currentPath = currentFile.parents[0]

    parent_submaps = [submap for submap in get_submap_parents(submap_node) if submap.type == "submap" and hasattr(submap, "filename")] # submaps with filename

    parent_dirs = [Path(getattr(parent_submap, "filename").value).parent for parent_submap in parent_submaps] # get paths without file at the end

    parent_dirs.append(currentPath) # add absolute root path

    parent_dirs.reverse() # reverse, so root path is first one

    parent_path = reduce(lambda p1, p2: p1 / p2, parent_dirs).resolve()

    return parent_path


def get_submap_parents(submap):
    parents = []
    parent = submap.parent
    while parent is not None:
        parents.append(parent)
        parent = parent.parent
    return parents[:-1]

def on_save(main_window):
    # should reload other submaps as well
    choice = reksio.QMessageBox.StandardButton.Save

    nodes_model = main_window.getNodesModel()
    root_node = nodes_model.getRoot()

    #submap_list = lookup(root_node, "submap")
    submap_list = lookup_postorder(root_node, "submap")
    submaps_semver = dict()

    already_processes_indexes = list()

    for submap in submap_list:
        # get modified indexes every time
        # modified_indexes = nodes_model.getModifiedIndexes()
        modified_indexes = [index for index in nodes_model.getModifiedIndexes() if index not in already_processes_indexes]
        memory_map_node = submap.getChildByType("memory-map")
        if not memory_map_node:
            # not loaded submap?
            continue
        memory_map_index = nodes_model.getIndex(memory_map_node)

        submap_index = nodes_model.getIndex(submap)

        submap_modified = is_submap_modified(main_window, submap, modified_indexes)
        submap_semver_update = submap in submaps_semver.keys()

        if submap_modified or submap_semver_update:
            #print(f"submap {submap} map was changed")
            if choice != reksio.QMessageBox.StandardButton.SaveAll:
                working_path = get_submap_working_path(submap, main_window)
                filename = submap["filename"].value.replace("\\", "/")
                filename = (working_path / filename).resolve()

                choice = main_window.getSaveSubmapConfirmation(str(filename))
            if choice in (reksio.QMessageBox.StandardButton.Save, reksio.QMessageBox.StandardButton.SaveAll):
                # user wants to save
                # if submap_modified:
                    # # submap was modified by the user
                    # # TODO: if also submap_semver_update, then lower values than already there shouldn't be possible to select
                    # # i.e there's already major change, but user selects patch
                    # semver_propagation = get_semantic_version_from_user(main_window, submap.name, submaps_semver.get(submap, None))
                # else:
                    # # version was propagated
                    # semver_propagation = submaps_semver[submap]
                # if semver_propagation not in ["cancel", "ignore"]:
                    # # save pending changes
                    # for parent in get_submap_parents(submap):
                        # if parent not in submaps_semver:
                            # submaps_semver[parent] = semver_propagation
                        # else:
                            # existing_change = submaps_semver[parent]
                            # if semver_propagation == "major":
                                # # major overwrites all changes
                                # submaps_semver[parent] = "major"
                            # elif semver_propagation == "minor":
                                # # minor overwrites only patch
                                # if existing_change == "patch":
                                    # submaps_semver[parent] = "minor"
                            # else:
                                # # patch doesnt overwrite
                                # pass
                    # # update semver
                    # increment_semantic_version(main_window, semver_propagation, memory_map_node)
                    # increment_semantic_version(main_window, semver_propagation, submap)
                    # already_processes_indexes.add(memory_map_index)
                    # already_processes_indexes.add(submap_index)

                # elif semver_propagation == "cancel":
                    # return reksio.QMessageBox.StandardButton.Cancel

                save_submap(main_window, submap)
            elif choice in (reksio.QMessageBox.StandardButton.NoToAll, reksio.QMessageBox.StandardButton.Cancel):
                # discard all
                return choice

    # top_item = root_node.getChildByType("memory-map")
    # if top_item in submaps_semver:
        # semver_propagation = get_semantic_version_from_user(main_window, top_item.name, submaps_semver.get(top_item, None))
        # if semver_propagation not in ["cancel", "ignore"]:
            # increment_semantic_version(main_window, semver_propagation, top_item)
    return choice

def save_submap(main_window, submap):
    nodes_model = main_window.getNodesModel()
    #working_path = Path(str(main_window.currentFile)).parents[0]
    working_path = get_submap_working_path(submap, main_window)
    filename = submap["filename"].value.replace("\\", "/")
    filename = (working_path / filename).resolve()
    # we want to save a child
    memory_map_node = submap.getChildByType("memory-map")
    if memory_map_node:
        memory_map_node.savable = True
        try:
            index_to_save = nodes_model.getIndex(memory_map_node)
            reksio.ignoreNextFileModifiedSignalFromFile(str(filename))
            nodes_model.save(index_to_save, str(filename))
            reksio.info(f"Submap {submap.name} saved in {filename}.")
        except:
            reksio.critical(f"Failed to save {submap.name} in {filename}.")
        memory_map_node.savable = False

def printer(root_node, *args):
	print(root_node.type)
	for child in root_node.children():
		printer(child)
